<template>
	<div class="gantt-container">
		<el-card shadow="hover" :body-style="{ paddingBottom: '6px' }">
			<div class="gantt-toolbar">
				<div class="legend-group" v-if="levelLegends.length">
					<span class="legend-label">层级配色</span>
					<div v-for="lvl in levelLegends" :key="lvl" class="legend-chip dual">
						<span class="legend-color track plan" :style="{ background: getLevelColors(lvl).plan }" title="计划" />
						<span class="legend-color track actual" :style="{ background: getLevelColors(lvl).actual }" title="实际" />
						<span class="legend-text">L{{ lvl + 1 }}</span>
					</div>
				</div>
				<div class="toolbar-actions">
					<span class="toolbar-label">行高</span>
					<el-slider v-model="state.rowHeight" :min="28" :max="60" :step="2" style="width: 80px" />
					<span class="toolbar-label" style="margin-left: 12px">表格宽度</span>
					<el-slider v-model="state.tableWidth" :min="280" :max="800" :step="10" style="width: 80px" />
					<span class="toolbar-label" style="margin-left: 12px">显示模式</span>
					<el-select v-model="state.scaleMode" size="small" style="width: 100px" @change="applyScaleMode">
						<el-option label="自适应" value="auto" />
						<el-option label="每日" value="daily" />
					</el-select>
					<el-button size="small" @click="applyScaleMode" title="按当前模式重新适配">适配</el-button>
					<span class="toolbar-label" style="margin-left: 12px">刻度</span>
					<el-select v-model="state.granularity" size="small" style="width: 120px" @change="rebuildScaleHeader">
						<el-option label="自动" value="auto" />
						<el-option label="日" value="day" />
						<el-option label="周" value="week" />
						<el-option label="月" value="month" />
					</el-select>
					<el-tooltip content="显示轨道标签（条很窄时也显示）" placement="top">
						<el-switch v-model="state.showTrackLabels" active-text="标签" style="margin-left: 8px" />
					</el-tooltip>
					<el-button icon="ele-FullScreen" @click="fitOverview" title="全览（可横向滚动查看全部）" />
					<span class="toolbar-label" style="margin-left: 12px">批量展开</span>
					<el-select v-model="state.expandTarget" size="small" style="width: 120px" :disabled="state.bulkExpanding">
						<el-option :label="'展开到 L1'" :value="1" />
						<el-option v-for="lvl in dimsCount" :key="lvl" :label="'展开到 L' + lvl" :value="lvl" v-show="lvl > 1" />
						<el-option label="展开全部" :value="'all'" />
					</el-select>
					<el-button type="success" size="small" :loading="state.bulkExpanding" @click="handleBulkExpand" :disabled="state.bulkExpanding" title="按层级批量展开">
						{{ state.bulkExpanding ? '展开中...' : '展开' }}
					</el-button>
					<el-button size="small" @click="collapseAll" :disabled="state.bulkExpanding" title="收起全部">收起全部</el-button>
					<el-button type="primary" :loading="state.loading" icon="ele-Refresh" @click="reloadAll">刷新</el-button>
					<el-button icon="ele-Camera" :loading="state.exporting" @click="exportPng">导出图片</el-button>
					<span class="total-hint">共 {{ state.total }} 条</span>
				</div>
			</div>
		</el-card>

		<div class="gantt-wrapper" :class="{ 'is-auto-scale': state.scaleMode === 'auto' }" ref="ganttWrapperRef" :style="{ minHeight: ganttHeight + 'px' }">
			<!-- 自定义时间刻度（自动/手动：日/周/月），覆盖在右侧甘特图上方 -->
			<div v-show="scaleHeader.ticks.length" class="gantt-scale-header" :style="{ left: scaleHeader.left + 'px', width: scaleHeader.width + 'px' }">
				<div class="gantt-scale-content" :style="{ transform: 'scaleX(' + state.chartScaleX + ')' }">
					<div v-for="(tk, i) in scaleHeader.ticks" :key="i" class="gantt-scale-tick" :style="{ left: tk.left + 'px', width: tk.width + 'px' }">
						<span class="gantt-scale-label">{{ tk.label }}</span>
					</div>
				</div>
			</div>
			<vxe-gantt
				ref="ganttRef"
				v-bind="ganttOptions"
				:loading="state.loading"
				class="planview-gantt"
				:style="{ '--chart-scale-x': String(state.chartScaleX) }"
				@toggle-tree-expand="handleTreeExpand"
				@cell-click="handleCellClick"
			>
				<template #task-bar="{ row }">
					<div class="gantt-dual-track" :class="{ 'is-empty': !hasAnyTrack(row) }">
						<div class="gantt-track-row actual">
							<div v-if="hasTrack(row, 'actual')" :class="['gantt-track-bar', 'type-actual']" :title="row.tracks.actual.rangeText || '实际未设置周期'" :style="getTrackStyle(row, 'actual')">
								<span v-if="shouldShowTrackLabel(row.tracks.actual)" class="track-label">{{ trackLabelMap.actual }}</span>
							</div>
							<span v-else class="gantt-track-placeholder">无实际</span>
						</div>
						<div class="gantt-track-row plan">
							<div v-if="hasTrack(row, 'plan')" :class="['gantt-track-bar', 'type-plan']" :title="row.tracks.plan.rangeText || '计划未设置周期'" :style="getTrackStyle(row, 'plan')">
								<span class="track-label is-plan">{{ trackLabelMap.plan }}</span>
							</div>
							<span v-else class="gantt-track-placeholder">无计划</span>
						</div>
					</div>
				</template>
			</vxe-gantt>
		</div>
		<div class="gantt-footer" v-if="state.total > state.pageSize">
			<el-pagination background small layout="total, prev, pager, next" :total="state.total" :current-page="state.pageIndex" :page-size="state.pageSize" @current-change="handlePageChange" />
		</div>
	</div>
</template>

<script lang="ts" setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import html2canvas from 'html2canvas';
import { usePlanViewApi } from '/@/api/plans/planView';

const props = defineProps<{
	filters: any;
	dims: string[];
	timeField: 'both' | 'plan' | 'actual';
	timeMatch: 'intersects' | 'startWithin' | 'endWithin';
}>();

type ApiNode = {
	id: string;
	label: string;
	level: number;
	keyPath?: string[];
	displayPath?: string[];
	hasChildren?: boolean;
	planStart?: string;
	planEnd?: string;
	actualStart?: string;
	actualEnd?: string;
	progressPercent?: number | null;
	progress?: number | null;
	children?: ApiNode[];
};

type GanttRecord = {
	id: string;
	parentId: string | null;
	title: string;
	level: number;
	children?: GanttRecord[]; // for nested tree mode
	start: string;
	end: string;
	planStart?: string;
	planEnd?: string;
	actualStart?: string;
	actualEnd?: string;
	progress: number | null;
	planRange: string;
	actualRange: string;
	progressText: string;
	pathText: string;
	isGroup: boolean;
	hasChildren: boolean;
	childrenLoaded?: boolean;
	tracks: {
		plan: TrackSegment;
		actual: TrackSegment;
	};
	keyPath: string[];
};

type TrackType = 'plan' | 'actual';

type TrackSegment = {
	type: TrackType;
	hasData: boolean;
	offsetPercent: number;
	widthPercent: number;
	start?: string;
	end?: string;
	rangeText: string;
};

const planViewApi = usePlanViewApi();
const ganttRef = ref<any>(null);
const ganttWrapperRef = ref<HTMLElement | null>(null);
const levelColorPalette = [
	{ plan: '#A7D7FE', actual: '#1F78D1' },
	{ plan: '#B7E5CC', actual: '#37B26C' },
	{ plan: '#FFD6B3', actual: '#F08C38' },
	{ plan: '#DCC8FB', actual: '#8E63D8' },
	{ plan: '#F5C0CB', actual: '#E2558F' },
];
const loadedNodeKeys = new Set<string>();
const loadingNodeKeys = new Set<string>(); // guard concurrent loads to avoid duplicates
const expandedKeys = new Set<string>(); // persist expanded rows across reload
let suspendToggle = false; // ignore toggle events during programmatic reload/expand

const state = reactive({
	loading: false,
	exporting: false,
	total: 0,
	rowHeight: 36,
	showTrackLabels: false,
	scaleMode: 'auto' as 'auto' | 'daily',
	granularity: 'auto' as 'auto' | 'day' | 'week' | 'month',
	chartScaleX: 1,
	bulkExpanding: false,
	expandTarget: 1 as number | 'all',
	pageIndex: 1,
	pageSize: 20,
	rows: [] as GanttRecord[],
	timeStart: '',
	timeEnd: '',
	tableWidth: 540,
});

const dimsCount = computed(() => Math.max(Array.isArray(props.dims) ? props.dims.length : 1, 1));

// 自定义刻度头部（与右侧图对齐）
const scaleHeader = reactive({
	left: 0,
	width: 0,
	ticks: [] as Array<{ left: number; width: number; label: string }>,
});

const requestBody = computed(() => ({
	filters: props.filters,
	timeField: props.timeField,
	timeMatch: props.timeMatch,
	dims: props.dims,
	pageIndex: state.pageIndex,
	pageSize: state.pageSize,
	parentKeyPath: undefined,
	lazy: true,
}));

const ganttHeight = computed(() => Math.max(420, state.rows.length * state.rowHeight + 200));

const baseColumns = [
	{ field: 'title', title: '任务 / 维度路径', treeNode: true, minWidth: 280, showOverflow: true },
	{ field: 'planRange', title: '计划周期', width: 200, showOverflow: true },
	{ field: 'actualRange', title: '实际周期', width: 200, showOverflow: true },
	{ field: 'progressText', title: '进度', width: 100, align: 'center' },
	{ field: 'pathText', title: '维度路径', minWidth: 260, showOverflow: true },
];

const ganttOptions = computed(() => ({
	showOverflow: true,
	border: 'outer',
	stripe: true,
	align: 'left',
	height: ganttHeight.value,
	rowConfig: { useKey: true, keyField: 'id' },
	cellConfig: { height: state.rowHeight },
	columnConfig: { resizable: true },
	treeConfig: {
		// 使用嵌套 children 作为树数据源，避免 transform + parentId 在 gantt 右侧不同步
		transform: false,
		rowField: 'id',
		childrenField: 'children',
		showLine: true,
		expandAll: false,
		accordion: false, // 禁止手风琴：展开一个不收起其他
		reserve: true, // 保留展开状态，reloadData 后仍保持
		// 不使用内置的行/单元格触发展开，改为我们在 cell-click 中手动控制，避免首次点击时先展开后又收起
		lazy: false,
		hasChildField: 'hasChildren',
	},
	taskBarConfig: {
		showProgress: false,
		showContent: false,
		barStyle: () => ({
			bgColor: 'transparent',
			completedBgColor: 'transparent',
			round: true,
		}),
	},
	taskViewConfig: { tableStyle: { width: state.tableWidth } },
	columns: baseColumns,
	// 嵌套树源
	data: state.rows,
}));

const levelLegends = computed(() => {
	const set = new Set<number>();
	state.rows.forEach((row: { level: number }) => set.add(row.level));
	return Array.from(set).sort((a, b) => a - b);
});

const getLevelColors = (level: number) => levelColorPalette[(level ?? 0) % levelColorPalette.length];

const trackLabelMap: Record<TrackType, string> = {
	plan: '计划',
	actual: '实际',
};

const TRACK_MIN_WIDTH_PERCENT = 1;
const DAY_MS = 24 * 60 * 60 * 1000;

function getPathKey(path?: string[]) {
	if (!path?.length) return '';
	return path.join('|');
}

function resetLazyState() {
	loadedNodeKeys.clear();
	state.rows.forEach((row) => {
		row.childrenLoaded = false;
	});
}

function getTimestamp(value?: string) {
	if (!value) return null;
	const t = Date.parse(value);
	return Number.isNaN(t) ? null : t;
}

function clampPercent(value: number) {
	if (!Number.isFinite(value)) return 0;
	return Math.max(0, Math.min(100, value));
}

function pickDateByMs(targetMs: number | null, ...values: (string | undefined)[]) {
	if (targetMs == null) return '';
	for (const value of values) {
		if (!value) continue;
		const ms = getTimestamp(value);
		if (ms === targetMs) {
			return value;
		}
	}
	return values.find(Boolean) || '';
}

function buildTrackSegment(type: TrackType, start: string | undefined, end: string | undefined, timelineStartMs: number | null, timelineEndMs: number | null): TrackSegment {
	if (!start && !end) {
		return {
			type,
			hasData: false,
			offsetPercent: 0,
			widthPercent: 0,
			rangeText: '',
		};
	}
	const startMs = getTimestamp(start);
	const endMs = getTimestamp(end);
	if (timelineStartMs == null || timelineEndMs == null) {
		return {
			type,
			hasData: !!(startMs || endMs),
			offsetPercent: 0,
			widthPercent: 100,
			start,
			end,
			rangeText: formatRange(start, end),
		};
	}
	const safeStartMs = startMs ?? endMs ?? timelineStartMs;
	let safeEndMs = endMs ?? startMs ?? safeStartMs;
	if (safeEndMs <= safeStartMs) {
		safeEndMs = safeStartMs + Math.max(Math.round((timelineEndMs - timelineStartMs) * 0.02), 1);
	}
	const duration = Math.max(timelineEndMs - timelineStartMs, 1);
	const offsetPercent = clampPercent(((safeStartMs - timelineStartMs) / duration) * 100);
	const widthPercent = clampPercent(((safeEndMs - safeStartMs) / duration) * 100);
	return {
		type,
		hasData: true,
		offsetPercent,
		widthPercent: Math.max(widthPercent, TRACK_MIN_WIDTH_PERCENT),
		start,
		end,
		rangeText: formatRange(start, end),
	};
}

function hasTrack(row: GanttRecord, type: TrackType) {
	return !!row?.tracks?.[type]?.hasData;
}

function hasAnyTrack(row: GanttRecord) {
	return hasTrack(row, 'plan') || hasTrack(row, 'actual');
}

function getTrackStyle(row: GanttRecord, type: TrackType) {
	const track = row.tracks?.[type];
	if (!track?.hasData) return {};
	const colors = getLevelColors(row.level ?? 0);
	const color = colors[type];
	const textColor = type === 'actual' ? '#fff' : color;
	return {
		left: `${track.offsetPercent}%`,
		width: `${track.widthPercent}%`,
		background: color,
		color: textColor,
	};
}

function shouldShowTrackLabel(track?: TrackSegment) {
	if (!track || !track.hasData) return false;
	return state.showTrackLabels || track.widthPercent >= 8;
}

function createNodeId(node: ApiNode, parentId?: string | null) {
	if (node.id) return node.id;
	if (node.keyPath?.length) return node.keyPath.join('/');
	return `${parentId || 'node'}-${Math.random().toString(36).slice(2, 10)}`;
}

function normalizeProgressValue(value: any): number | null {
	if (value == null || value === '') return null;
	const num = Number(value);
	if (!isFinite(num)) return null;
	return Math.min(Math.max(num, 0), 100);
}

function formatProgressPercent(value?: number | null) {
	if (value == null || !isFinite(value)) return '-';
	return `${Math.round(Math.min(Math.max(Number(value), 0), 100))}%`;
}

function formatDateShort(value?: string) {
	if (!value) return '';
	return String(value).slice(0, 10);
}

function formatRange(start?: string, end?: string) {
	if (!start && !end) return '-';
	const s = formatDateShort(start);
	const e = formatDateShort(end);
	if (!s) return e || '-';
	if (!e) return s;
	return `${s} ~ ${e}`;
}

function formatPathText(parts?: string[]) {
	if (!parts?.length) return '';
	return parts.filter(Boolean).join(' / ');
}

// Build rows for the current level only. Do NOT recurse children here to keep the Gantt truly lazy-loaded.
function buildRecords(nodes: ApiNode[] = [], parentId: string | null = null, acc: GanttRecord[] = []): GanttRecord[] {
	nodes.forEach((node) => {
		// 使用 keyPath 以 '|' 连接作为稳定且安全的行主键，避免包含 '/' 导致的转义差异
		const safeKey = node.keyPath?.length ? node.keyPath.join('|') : '';
		const id = safeKey || createNodeId(node, parentId);
		const planStart = node.planStart || '';
		const planEnd = node.planEnd || '';
		const actualStart = node.actualStart || '';
		const actualEnd = node.actualEnd || '';
		const planStartMs = getTimestamp(planStart);
		const planEndMs = getTimestamp(planEnd);
		const actualStartMs = getTimestamp(actualStart);
		const actualEndMs = getTimestamp(actualEnd);
		const msCandidates = [planStartMs, planEndMs, actualStartMs, actualEndMs].filter((ms): ms is number => ms != null);
		let timelineStartMs: number | null = msCandidates.length ? Math.min(...msCandidates) : null;
		let timelineEndMs: number | null = msCandidates.length ? Math.max(...msCandidates) : null;
		if (timelineStartMs != null && timelineEndMs != null && timelineEndMs <= timelineStartMs) {
			timelineEndMs = timelineStartMs + DAY_MS;
		}
		const start = pickDateByMs(timelineStartMs, planStart, actualStart, planEnd, actualEnd);
		const end = pickDateByMs(timelineEndMs, actualEnd, planEnd, actualStart, planStart) || start;
		const progress = normalizeProgressValue(node.progressPercent ?? node.progress ?? null);
		const tracks = {
			plan: buildTrackSegment('plan', planStart, planEnd, timelineStartMs, timelineEndMs),
			actual: buildTrackSegment('actual', actualStart, actualEnd, timelineStartMs, timelineEndMs),
		};
		const record: GanttRecord = {
			id,
			parentId,
			title: node.label || '(未命名)',
			level: node.level ?? 0,
			start,
			end,
			planStart,
			planEnd,
			actualStart,
			actualEnd,
			progress,
			planRange: formatRange(planStart, planEnd),
			actualRange: formatRange(actualStart, actualEnd),
			progressText: formatProgressPercent(progress),
			pathText: formatPathText(node.displayPath?.length ? node.displayPath : node.keyPath),
			hasChildren: node.hasChildren ?? !!(node.children && node.children.length),
			isGroup: node.hasChildren ?? !!(node.children && node.children.length),
			tracks,
			childrenLoaded: Array.isArray(node.children) && node.children.length > 0,
			keyPath: node.keyPath?.slice() || [],
		};
		// 若后端已返回了子节点（批量展开），递归构建 children
		if (Array.isArray(node.children) && node.children.length > 0) {
			record.children = buildRecords(node.children, id, []);
		}
		acc.push(record);
	});
	return acc;
}

async function refresh() {
	state.loading = true;
	try {
		resetLazyState();
		const res = await planViewApi.getGantt(requestBody.value);
		const data = res.data?.result || res.data || {};
		const nodes = data.nodes || [];
		const totalNodes = Number(data.totalNodes ?? data.totalItems ?? nodes.length);
		state.total = totalNodes;
		const totalPages = Math.max(totalNodes > 0 ? Math.ceil(totalNodes / state.pageSize) : 1, 1);
		if (state.pageIndex > totalPages) {
			state.pageIndex = totalPages;
			return await refresh();
		}
		state.rows = buildRecords(nodes, null, []);
		state.timeStart = data.timeStart || '';
		state.timeEnd = data.timeEnd || '';
		await nextTick();
		const $gantt = ganttRef.value;
		suspendToggle = true;
		$gantt?.reloadData?.(state.rows);
		await nextTick();
		reExpandAll();
		suspendToggle = false;
		await nextTick();
		applyScaleMode();
	} finally {
		state.loading = false;
	}
}

function reloadAll() {
	resetLazyState();
	refresh();
}

function fitOverview() {
	// 尽量给右侧图形更多可视空间，并允许横向滚动查看全部
	state.tableWidth = Math.max(300, Math.min(state.tableWidth, 480));
	nextTick(async () => {
		const el = ganttWrapperRef.value;
		if (el) {
			// 滚动到最左
			el.scrollLeft = 0;
		}
		// 避免触发手风琴式折叠：暂时屏蔽切换事件，reload 后恢复展开
		const $gantt = ganttRef.value;
		if ($gantt?.reloadData) {
			suspendToggle = true;
			$gantt.reloadData(state.rows);
			await nextTick();
			reExpandAll();
			suspendToggle = false;
		}
		// 自适应缩放一次
		applyScaleMode();
	});
}

function collapseAll() {
	expandedKeys.clear();
	const $gantt = ganttRef.value;
	if ($gantt?.clearTreeExpand) {
		$gantt.clearTreeExpand();
	} else if ($gantt?.setAllTreeExpand) {
		$gantt.setAllTreeExpand(false);
	} else {
		// 兜底：reload 一次即可折叠
		$gantt?.reloadData?.(state.rows);
	}
}

async function ensureChildrenLoaded(row: GanttRecord) {
	if (!row.hasChildren) return;
	if (row.childrenLoaded) return;
	const cacheKey = getPathKey(row.keyPath);
	if (loadedNodeKeys.has(cacheKey) || loadingNodeKeys.has(cacheKey)) return;
	try {
		loadingNodeKeys.add(cacheKey);
		const children = await fetchChildRows(row);
		if (children && children.length) {
			insertChildren(row.id, children);
			loadedNodeKeys.add(cacheKey);
			row.childrenLoaded = true;
		} else {
			row.hasChildren = false;
		}
	} finally {
		loadingNodeKeys.delete(cacheKey);
	}
}

async function expandToDepth(target: number) {
	const maxDepth = Math.max(1, Math.min(target, dimsCount.value));
	// BFS：逐层加载到目标层级
	const $gantt = ganttRef.value;
	const queue: GanttRecord[] = [];
	const collect = (list: GanttRecord[]) => list.forEach((n) => queue.push(n));
	collect(state.rows);
	// 避免频繁重绘，批量处理后统一 reload + 恢复展开
	suspendToggle = true;
	state.bulkExpanding = true;
	try {
		while (queue.length) {
			const node = queue.shift()!;
			const depth = node.keyPath?.length || 0;
			if (depth >= maxDepth) continue;
			if (node.hasChildren) {
				await ensureChildrenLoaded(node);
				expandedKeys.add(node.id);
				if (node.children?.length) node.children.forEach((c) => queue.push(c));
			}
		}
		await nextTick();
		$gantt?.reloadData?.(state.rows);
		await nextTick();
		reExpandAll();
	} finally {
		state.bulkExpanding = false;
		suspendToggle = false;
	}
}

async function handleBulkExpand() {
	const target = state.expandTarget === 'all' ? dimsCount.value : Number(state.expandTarget || 1);
	// 优先使用后端批量展开，避免前端多次请求
	state.bulkExpanding = true;
	suspendToggle = true;
	try {
		const payload = {
			filters: props.filters,
			timeField: props.timeField,
			timeMatch: props.timeMatch,
			dims: props.dims,
			pageIndex: state.pageIndex,
			pageSize: state.pageSize,
			lazy: false,
			expandDepth: state.expandTarget === 'all' ? undefined : target,
			expandAll: state.expandTarget === 'all',
		} as any;
		const res = await planViewApi.getGantt(payload);
		const data = res.data?.result || res.data || {};
		const nodes = data.nodes || [];
		state.rows = buildRecords(nodes, null, []);
		state.timeStart = data.timeStart || '';
		state.timeEnd = data.timeEnd || '';
		// 标记展开 keys：把所有返回的节点 id 标记为展开（便于 reload 后保持）
		const markAllExpanded = (list: GanttRecord[]) => {
			list.forEach((n) => {
				expandedKeys.add(n.id);
				if (n.children?.length) markAllExpanded(n.children);
			});
		};
		expandedKeys.clear();
		markAllExpanded(state.rows);
		await nextTick();
		ganttRef.value?.reloadData?.(state.rows);
		await nextTick();
		reExpandAll();
	} finally {
		suspendToggle = false;
		state.bulkExpanding = false;
		await nextTick();
		applyScaleMode();
	}
}

function applyScaleMode() {
	// 每日模式：不缩放
	if (state.scaleMode === 'daily') {
		state.chartScaleX = 1;
		rebuildScaleHeader();
		return;
	}
	// 自适应：根据实际内容宽度压缩到容器宽度
	const wrap = ganttWrapperRef.value as HTMLElement | null;
	if (!wrap) {
		state.chartScaleX = 1;
		rebuildScaleHeader();
		return;
	}
	// 选择图表实际内容容器（多重选择以提升兼容性）
	const chartEl = (wrap.querySelector('.vxe-gantt-view--chart') as HTMLElement) || (wrap.querySelector('.vxe-gantt--chart') as HTMLElement) || (wrap.querySelector('.vxe-gantt') as HTMLElement);
	const wrapWidth = wrap.clientWidth || 1;
	const contentWidth = chartEl?.scrollWidth || wrap.scrollWidth || wrapWidth || wrapWidth;
	let scale = 1;
	if (contentWidth > 0) {
		scale = Math.min(1, wrapWidth / contentWidth);
	}
	state.chartScaleX = scale > 0 && isFinite(scale) ? scale : 1;
	rebuildScaleHeader();
}

function parseDate(value?: string): number | null {
	if (!value) return null;
	const t = Date.parse(value);
	return Number.isNaN(t) ? null : t;
}
function dayDiff(aMs: number, bMs: number) {
	const ONE = 24 * 3600 * 1000;
	return Math.max(0, Math.round((bMs - aMs) / ONE));
}
function startOfWeek(ms: number): number {
	const d = new Date(ms);
	const day = (d.getDay() + 6) % 7; // Monday=0
	d.setDate(d.getDate() - day);
	d.setHours(0, 0, 0, 0);
	return d.getTime();
}
function startOfMonth(ms: number): number {
	const d = new Date(ms);
	d.setDate(1);
	d.setHours(0, 0, 0, 0);
	return d.getTime();
}
function addDays(ms: number, days: number) {
	return ms + days * 24 * 3600 * 1000;
}
function addMonths(ms: number, months: number) {
	const d = new Date(ms);
	d.setMonth(d.getMonth() + months);
	return d.getTime();
}
function fmt(dateMs: number, gran: 'day' | 'week' | 'month') {
	const d = new Date(dateMs);
	if (gran === 'month') return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
	if (gran === 'week') {
		const m = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
		return `${m} W${Math.ceil(d.getDate() / 7)}`;
	}
	return `${d.getMonth() + 1}/${d.getDate()}`;
}

function pickGranularity(totalDays: number): 'day' | 'week' | 'month' {
	if (totalDays > 120) return 'month';
	if (totalDays > 45) return 'week';
	return 'day';
}

function rebuildScaleHeader() {
	// 仅在自适应模式显示头部刻度
	scaleHeader.ticks = [];
	const wrap = ganttWrapperRef.value as HTMLElement | null;
	if (!wrap) return;
	const chart = wrap.querySelector('.vxe-gantt-view--chart') as HTMLElement;
	const table = wrap.querySelector('.vxe-gantt-view--table') as HTMLElement;
	const chartEl = chart || (wrap.querySelector('.vxe-gantt--chart') as HTMLElement);
	const tableEl = table || (wrap.querySelector('.vxe-gantt--table') as HTMLElement);
	if (!chartEl) return;
	// 头部左偏移=表格宽度（若存在）
	const leftOffset = tableEl ? tableEl.getBoundingClientRect().width : 0;
	scaleHeader.left = leftOffset + 12; // wrapper padding: 12
	const fullWidth = chartEl.scrollWidth || chartEl.clientWidth;
	scaleHeader.width = fullWidth;
	// 计算时间范围
	const sMs = parseDate(state.timeStart);
	const eMs = parseDate(state.timeEnd);
	if (sMs == null || eMs == null || eMs <= sMs) return;
	const totalDays = dayDiff(sMs, eMs) || 1;
	let gran: 'day' | 'week' | 'month' = pickGranularity(totalDays);
	if (state.granularity !== 'auto') {
		gran = state.granularity as 'day' | 'week' | 'month';
	}
	// 生成刻度
	const ticks: Array<{ left: number; width: number; label: string }> = [];
	const duration = eMs - sMs;
	const toX = (ms: number) => Math.max(0, Math.min(1, (ms - sMs) / duration)) * fullWidth;
	if (gran === 'day') {
		// 日粒度：间隔取整（避免太密，最多放置 ~ 20 个标签）
		const step = Math.max(1, Math.ceil(totalDays / 20));
		let cur = sMs;
		while (cur < eMs) {
			const next = addDays(cur, step);
			const x = toX(cur);
			const w = Math.max(1, toX(Math.min(next, eMs)) - x);
			ticks.push({ left: Math.round(x), width: Math.round(w), label: fmt(cur, 'day') });
			cur = next;
		}
	} else if (gran === 'week') {
		let cur = startOfWeek(sMs);
		while (cur < eMs) {
			const next = addDays(cur, 7);
			const x = toX(cur);
			const w = Math.max(1, toX(Math.min(next, eMs)) - x);
			ticks.push({ left: Math.round(x), width: Math.round(w), label: fmt(cur, 'week') });
			cur = next;
		}
	} else {
		let cur = startOfMonth(sMs);
		while (cur < eMs) {
			const next = addMonths(cur, 1);
			const x = toX(cur);
			const w = Math.max(1, toX(Math.min(next, eMs)) - x);
			ticks.push({ left: Math.round(x), width: Math.round(w), label: fmt(cur, 'month') });
			cur = next;
		}
	}
	scaleHeader.ticks = ticks;
}

function handlePageChange(page: number) {
	if (page === state.pageIndex) return;
	state.pageIndex = page;
	resetLazyState();
	refresh();
}

function insertChildren(parentId: string, children: GanttRecord[]) {
	if (!children.length) return;
	// 在嵌套树中查找父节点
	const findNode = (list: GanttRecord[], id: string): GanttRecord | null => {
		for (const n of list) {
			if (n.id === id) return n;
			if (n.children?.length) {
				const f = findNode(n.children, id);
				if (f) return f;
			}
		}
		return null;
	};
	const parent = findNode(state.rows, parentId);
	if (!parent) {
		// 未找到父节点，则作为根插入（降级）
		const existingIds = new Set(state.rows.map((r) => r.id));
		const toInsert = children.filter((c) => !existingIds.has(c.id));
		if (!toInsert.length) return;
		state.rows.push(...toInsert);
	} else {
		// 去重
		const existingIds = new Set((parent.children || []).map((r) => r.id));
		const toInsert = children.filter((c) => !existingIds.has(c.id));
		if (!toInsert.length) return;
		parent.children = (parent.children || []).concat(toInsert);
		parent.hasChildren = true;
	}
	// 触发响应
	state.rows = state.rows.slice();
}

function reExpandAll() {
	const $gantt = ganttRef.value;
	if (!$gantt?.setTreeExpand) return;
	// 逐个恢复展开状态（忽略未渲染/未找到的行）
	expandedKeys.forEach((id) => {
		const r = $gantt.getRowById ? $gantt.getRowById(id) : null;
		if (r) $gantt.setTreeExpand(r, true);
	});
}

async function fetchChildRows(row: GanttRecord) {
	const payload = {
		...requestBody.value,
		parentKeyPath: row.keyPath,
		pageIndex: 1,
		pageSize: 1000,
		lazy: true,
	};
	const res = await planViewApi.getGantt(payload);
	const data = res.data?.result || res.data || {};
	const nodes = data.nodes || [];
	return buildRecords(nodes, row.id, []);
}

async function handleTreeExpand(params: { row: GanttRecord; expanded: boolean }) {
	const { row, expanded } = params;
	console.log('[PlanView][Gantt] toggle-tree-expand', { id: row?.id, expanded, hasChildren: row?.hasChildren, childrenLoaded: row?.childrenLoaded });
	if (suspendToggle) return; // 忽略自动切换事件，避免误判为收起
	// 维护展开集合，保持多节点同时展开
	if (expanded) expandedKeys.add(row.id);
	else expandedKeys.delete(row.id);
	if (!expanded || !row.hasChildren) return;
	if (row.childrenLoaded) return;
	const cacheKey = getPathKey(row.keyPath);
	if (loadedNodeKeys.has(cacheKey) || loadingNodeKeys.has(cacheKey)) return;
	state.loading = true;
	try {
		loadingNodeKeys.add(cacheKey);
		const children = await fetchChildRows(row);
		console.log('[PlanView][Gantt] fetched children', { parent: row.id, count: children.length });
		if (!children.length) {
			row.hasChildren = false;
			return;
		}
		// 直接就地插入本地数据源，随后强制 reloadData，保证 UI 与数据一致
		console.log('[PlanView][Gantt] insertChildren + reloadData');
		insertChildren(row.id, children);
		await nextTick();
		const $gantt = ganttRef.value;
		suspendToggle = true;
		$gantt?.reloadData?.(state.rows);
		await nextTick();
		// 尝试通过 id 重新获取行对象再展开，避免引用失效
		const cur = $gantt?.getRowById ? $gantt.getRowById(row.id) : row;
		if ($gantt?.setTreeExpand) $gantt.setTreeExpand(cur, true);
		// 恢复其他已展开节点，避免“手风琴”式折叠
		reExpandAll();
		suspendToggle = false;
		await nextTick();
		applyScaleMode();
		loadedNodeKeys.add(cacheKey);
		row.childrenLoaded = true;
	} catch (error) {
		row.childrenLoaded = false;
		console.error('[PlanView][Gantt] load children failed', error);
	} finally {
		loadingNodeKeys.delete(cacheKey);
		state.loading = false;
	}
}

// 允许点击左侧“任务/维度路径”单元格进行下钻/展开
async function handleCellClick(params: { row: GanttRecord; column: any }) {
	const { row, column } = params || {};
	console.log('[PlanView][Gantt] cell-click', { col: column?.field, id: row?.id, hasChildren: row?.hasChildren, childrenLoaded: row?.childrenLoaded });
	// 仅在点击首列（title）时处理，避免误触其它列
	if (!row || !column || column.field !== 'title') return;
	if (!row.hasChildren) return;
	const $gantt = ganttRef.value;
	// 若尚未加载过子节点，先加载再展开，确保有数据可见
	if (!row.childrenLoaded) {
		try {
			state.loading = true;
			const cacheKey = getPathKey(row.keyPath);
			if (!loadedNodeKeys.has(cacheKey) && !loadingNodeKeys.has(cacheKey)) {
				loadingNodeKeys.add(cacheKey);
				const children = await fetchChildRows(row);
				console.log('[PlanView][Gantt] fetched children (click)', { parent: row.id, count: children?.length || 0 });
				if (children && children.length) {
					console.log('[PlanView][Gantt] insertChildren + reloadData (click)');
					insertChildren(row.id, children);
					await nextTick();
					const $gantt = ganttRef.value;
					suspendToggle = true;
					$gantt?.reloadData?.(state.rows);
					await nextTick();
					const cur = $gantt?.getRowById ? $gantt.getRowById(row.id) : row;
					if ($gantt?.setTreeExpand) {
						$gantt.setTreeExpand(cur, true);
						expandedKeys.add(row.id);
					}
					reExpandAll();
					suspendToggle = false;
					await nextTick();
					applyScaleMode();
					loadedNodeKeys.add(cacheKey);
					row.childrenLoaded = true;
				} else {
					// 无子数据，标记成无子节点，避免反复点击请求
					row.hasChildren = false;
					return;
				}
			} else if (!loadedNodeKeys.has(cacheKey) && loadingNodeKeys.has(cacheKey)) {
				// 若正在加载，暂不重复触发
				return;
			} else {
				// 无子数据，标记成无子节点，避免反复点击请求
				// 已加载过，继续向下
			}
		} catch (e) {
			console.error('[PlanView][Gantt] click-to-expand failed', e);
			return;
		} finally {
			loadingNodeKeys.delete(getPathKey(row.keyPath));
			state.loading = false;
		}
	}
	// 展开/折叠行（不同版本API兼容）
	// 优先使用强制展开，避免因双事件导致二次“切换”而折叠
	if ($gantt?.setTreeExpand) {
		$gantt.setTreeExpand(row, true);
	} else if ($gantt?.toggleTreeExpand) {
		$gantt.toggleTreeExpand(row);
	}
}

async function exportPng() {
	if (!ganttWrapperRef.value) return;
	state.exporting = true;
	try {
		const canvas = await html2canvas(ganttWrapperRef.value, {
			backgroundColor: '#fff',
			scale: window.devicePixelRatio > 1 ? 2 : 1,
			useCORS: true,
		});
		const url = canvas.toDataURL('image/png');
		const link = document.createElement('a');
		link.href = url;
		link.download = 'planview-gantt.png';
		link.click();
	} finally {
		state.exporting = false;
	}
}

onMounted(async () => {
	await refresh();
	// 监听窗口变化，自动适配（自适应模式）
	const onResize = () => {
		if (state.scaleMode === 'auto') applyScaleMode();
	};
	(window as any).__planview_gantt_resize = onResize;
	window.addEventListener('resize', onResize);
});

watch(
	() => [props.filters, props.dims, props.timeField, props.timeMatch],
	async () => {
		state.pageIndex = 1;
		resetLazyState();
		await refresh();
	},
	{ deep: true }
);

// 清理 resize 监听
import { onBeforeUnmount } from 'vue';
onBeforeUnmount(() => {
	const fn = (window as any).__planview_gantt_resize;
	if (fn) window.removeEventListener('resize', fn);
	(window as any).__planview_gantt_resize = null;
});
</script>

<style scoped>
.gantt-container :deep(.el-card__body) {
	padding-top: 8px;
}
.gantt-toolbar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	flex-wrap: wrap;
}
.legend-group {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 6px;
	font-size: 12px;
	color: var(--el-text-color-secondary);
}
.legend-label {
	font-weight: 600;
}
.legend-chip {
	display: flex;
	align-items: center;
	gap: 4px;
	padding: 2px 8px;
	border: 1px solid var(--el-border-color-lighter);
	border-radius: 999px;
}
.legend-color {
	width: 12px;
	height: 12px;
	border-radius: 50%;
	display: inline-block;
}
.legend-chip.dual {
	gap: 6px;
}
.legend-color.track {
	width: 14px;
	height: 6px;
	border-radius: 999px;
}
.legend-color.track.plan {
	box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12);
}
.legend-color.track.actual {
	box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.2);
}
.legend-text {
	font-size: 12px;
	color: var(--el-text-color-regular);
}
.toolbar-actions {
	display: flex;
	align-items: center;
	gap: 10px;
	flex-wrap: wrap;
	margin-left: auto;
}
.toolbar-label {
	font-size: 12px;
	color: var(--el-text-color-secondary);
}
.total-hint {
	color: #909399;
	font-size: 12px;
}
.gantt-wrapper {
	margin-top: 12px;
	border: 1px solid var(--el-border-color);
	border-radius: 8px;
	background: var(--el-fill-color-blank);
	padding: 12px;
	box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
	/* 默认横纵滚动 */
	overflow-y: auto;
	overflow-x: auto;
}
.gantt-wrapper.is-auto-scale {
	overflow-x: hidden;
}
.planview-gantt {
	width: 100%;
	/* 通过 CSS 变量实现横向压缩，自适应填满容器 */
	--chart-scale-x: 1;
}
.planview-gantt :deep(.vxe-gantt-view--chart) {
	transform-origin: left top;
	transform: scaleX(var(--chart-scale-x));
}
.gantt-wrapper.is-auto-scale :deep(.vxe-gantt--chart) {
	overflow-x: hidden;
}
.gantt-wrapper.is-auto-scale :deep(.vxe-gantt-view--chart) {
	overflow-x: hidden;
}
.gantt-wrapper.is-auto-scale :deep(.vxe-gantt--main) {
	overflow-x: hidden;
}
.planview-gantt :deep(.vxe-gantt-view--chart-custom-bar) {
	position: relative;
	height: 100%;
}

/* 自定义刻度头部样式 */
.gantt-scale-header {
	position: absolute;
	top: 12px; /* 与 wrapper 内边距对齐 */
	height: 22px;
	pointer-events: none;
	z-index: 10;
}
.gantt-scale-content {
	position: relative;
	height: 100%;
}
.gantt-scale-tick {
	position: absolute;
	top: 0;
	bottom: 0;
	border-left: 1px solid rgba(15, 23, 42, 0.08);
}
.gantt-scale-label {
	position: absolute;
	top: 2px;
	left: 4px;
	font-size: 12px;
	color: #64748b;
	background: rgba(255, 255, 255, 0.8);
	padding: 0 2px;
	border-radius: 2px;
}
.gantt-dual-track {
	position: absolute;
	inset: 4px;
	--plan-track-height: 12px;
	--actual-track-height: 22px;
	display: block;
}
.gantt-dual-track.is-empty {
	justify-content: center;
}
.gantt-track-row {
	position: absolute;
	left: 0;
	right: 0;
	border-radius: 999px;
	background: transparent;
	overflow: hidden;
	font-size: 11px;
	color: #64748b;
	display: flex;
	align-items: center;
	justify-content: center;
}
.gantt-track-row.plan {
	height: var(--plan-track-height);
	top: 0;
	z-index: 2;
}
.gantt-track-row.actual {
	height: var(--actual-track-height);
	top: calc(var(--plan-track-height) / 2);
	z-index: 1;
}
.gantt-track-bar {
	position: absolute;
	border-radius: 999px;
	display: flex;
	align-items: center;
	padding: 0 8px;
	font-size: 12px;
	font-weight: 600;
	white-space: nowrap;
	box-shadow: none;
}
.gantt-track-bar.type-actual {
	top: 2px;
	bottom: 2px;
}
.gantt-track-bar.type-plan {
	top: 50%;
	bottom: auto;
	height: 4px;
	transform: translateY(-50%);
	padding: 0;
	box-shadow: none;
}
.gantt-track-bar.type-plan::before,
.gantt-track-bar.type-plan::after {
	content: '';
	position: absolute;
	top: 50%;
	width: 10px;
	height: 10px;
	border-radius: 50%;
	background: currentColor;
	left: -5px;
	transform: translateY(-50%);
}
.gantt-track-bar.type-plan::after {
	left: auto;
	right: -5px;
}
.gantt-track-row.plan .gantt-track-bar {
	color: #0f172a;
}
.gantt-track-row.actual .gantt-track-bar {
	color: #fff;
}
.track-label {
	font-weight: 600;
}
.track-label.is-plan {
	position: absolute;
	right: -6px;
	top: -2px;
	transform: translateX(100%);
	font-size: 12px;
	color: currentColor;
	background: var(--el-fill-color-blank);
	padding-left: 6px;
}
.gantt-track-placeholder {
	font-size: 12px;
	color: rgba(15, 23, 42, 0.5);
}
.gantt-footer {
	margin-top: 8px;
	display: flex;
	justify-content: flex-end;
}
</style>
